// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/base/text_renderer.h"

#include <stddef.h>

#include <utility>

#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/logging.h"
#include "base/single_thread_task_runner.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/decoder_buffer.h"
#include "media/base/demuxer.h"
#include "media/base/text_cue.h"

namespace media {

TextRenderer::TextRenderer(
    const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
    const AddTextTrackCB& add_text_track_cb)
    : task_runner_(task_runner)
    , add_text_track_cb_(add_text_track_cb)
    , state_(kUninitialized)
    , pending_read_count_(0)
    , weak_factory_(this)
{
}

TextRenderer::~TextRenderer()
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    text_track_state_map_.clear();
    if (!pause_cb_.is_null())
        base::ResetAndReturn(&pause_cb_).Run();
}

void TextRenderer::Initialize(const base::Closure& ended_cb)
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK(!ended_cb.is_null());
    DCHECK_EQ(kUninitialized, state_);// << "state_ " << state_;
    DCHECK(text_track_state_map_.empty());
    DCHECK_EQ(pending_read_count_, 0);
    DCHECK(pending_eos_set_.empty());
    DCHECK(ended_cb_.is_null());

    ended_cb_ = ended_cb;
    state_ = kPaused;
}

void TextRenderer::StartPlaying()
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK_EQ(state_, kPaused);// << "state_ " << state_;

    for (auto itr = text_track_state_map_.begin();
         itr != text_track_state_map_.end(); ++itr) {
        TextTrackState* state = itr->second.get();
        if (state->read_state == TextTrackState::kReadPending) {
            DCHECK_GT(pending_read_count_, 0);
            continue;
        }

        Read(state, itr->first);
    }

    state_ = kPlaying;
}

void TextRenderer::Pause(const base::Closure& callback)
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK(state_ == kPlaying || state_ == kEnded) << "state_ " << state_;
    DCHECK_GE(pending_read_count_, 0);

    if (pending_read_count_ == 0) {
        state_ = kPaused;
        task_runner_->PostTask(FROM_HERE, callback);
        return;
    }

    pause_cb_ = callback;
    state_ = kPausePending;
}

void TextRenderer::Flush(const base::Closure& callback)
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK_EQ(pending_read_count_, 0);
    DCHECK(state_ == kPaused) << "state_ " << state_;

    for (auto itr = text_track_state_map_.begin();
         itr != text_track_state_map_.end(); ++itr) {
        pending_eos_set_.insert(itr->first);
        itr->second->text_ranges_.Reset();
    }
    DCHECK_EQ(pending_eos_set_.size(), text_track_state_map_.size());
    task_runner_->PostTask(FROM_HERE, callback);
}

void TextRenderer::AddTextStream(DemuxerStream* text_stream,
    const TextTrackConfig& config)
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK(state_ != kUninitialized) << "state_ " << state_;
    DCHECK(text_track_state_map_.find(text_stream) == text_track_state_map_.end());
    DCHECK(pending_eos_set_.find(text_stream) == pending_eos_set_.end());

    AddTextTrackDoneCB done_cb = BindToCurrentLoop(base::Bind(&TextRenderer::OnAddTextTrackDone,
        weak_factory_.GetWeakPtr(),
        text_stream));

    add_text_track_cb_.Run(config, done_cb);
}

void TextRenderer::RemoveTextStream(DemuxerStream* text_stream)
{
    DCHECK(task_runner_->BelongsToCurrentThread());

    auto itr = text_track_state_map_.find(text_stream);
    DCHECK(itr != text_track_state_map_.end());

    TextTrackState* state = itr->second.get();
    DCHECK_EQ(state->read_state, TextTrackState::kReadIdle);
    text_track_state_map_.erase(itr);

    pending_eos_set_.erase(text_stream);
}

bool TextRenderer::HasTracks() const
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    return !text_track_state_map_.empty();
}

void TextRenderer::BufferReady(
    DemuxerStream* stream,
    DemuxerStream::Status status,
    const scoped_refptr<DecoderBuffer>& input)
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK_NE(status, DemuxerStream::kConfigChanged);

    if (status == DemuxerStream::kAborted) {
        DCHECK(!input.get());
        DCHECK_GT(pending_read_count_, 0);
        DCHECK(pending_eos_set_.find(stream) != pending_eos_set_.end());

        auto itr = text_track_state_map_.find(stream);
        DCHECK(itr != text_track_state_map_.end());

        TextTrackState* state = itr->second.get();
        DCHECK_EQ(state->read_state, TextTrackState::kReadPending);

        --pending_read_count_;
        state->read_state = TextTrackState::kReadIdle;

        switch (state_) {
        case kPlaying:
            return;

        case kPausePending:
            if (pending_read_count_ == 0) {
                state_ = kPaused;
                base::ResetAndReturn(&pause_cb_).Run();
            }

            return;

        case kPaused:
        case kUninitialized:
        case kEnded:
            NOTREACHED();
            return;
        }

        NOTREACHED();
        return;
    }

    if (input->end_of_stream()) {
        CueReady(stream, NULL);
        return;
    }

    DCHECK_EQ(status, DemuxerStream::kOk);
    DCHECK_GE(input->side_data_size(), 2u);

    // The side data contains both the cue id and cue settings,
    // each terminated with a NUL.
    const char* id_ptr = reinterpret_cast<const char*>(input->side_data());
    size_t id_len = strlen(id_ptr);
    std::string id(id_ptr, id_len);

    const char* settings_ptr = id_ptr + id_len + 1;
    size_t settings_len = strlen(settings_ptr);
    std::string settings(settings_ptr, settings_len);

    // The cue payload is stored in the data-part of the input buffer.
    std::string text(input->data(), input->data() + input->data_size());

    scoped_refptr<TextCue> text_cue(
        new TextCue(input->timestamp(),
            input->duration(),
            id,
            settings,
            text));

    CueReady(stream, text_cue);
}

void TextRenderer::CueReady(
    DemuxerStream* text_stream,
    const scoped_refptr<TextCue>& text_cue)
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK_NE(state_, kUninitialized);
    DCHECK_GT(pending_read_count_, 0);
    DCHECK(pending_eos_set_.find(text_stream) != pending_eos_set_.end());

    auto itr = text_track_state_map_.find(text_stream);
    DCHECK(itr != text_track_state_map_.end());

    TextTrackState* state = itr->second.get();
    DCHECK_EQ(state->read_state, TextTrackState::kReadPending);
    DCHECK(state->text_track);

    --pending_read_count_;
    state->read_state = TextTrackState::kReadIdle;

    switch (state_) {
    case kPlaying: {
        if (text_cue.get())
            break;

        const size_t count = pending_eos_set_.erase(text_stream);
        DCHECK_EQ(count, 1U);

        if (pending_eos_set_.empty()) {
            DCHECK_EQ(pending_read_count_, 0);
            state_ = kEnded;
            task_runner_->PostTask(FROM_HERE, ended_cb_);
            return;
        }

        DCHECK_GT(pending_read_count_, 0);
        return;
    }
    case kPausePending: {
        if (text_cue.get())
            break;

        const size_t count = pending_eos_set_.erase(text_stream);
        DCHECK_EQ(count, 1U);

        if (pending_read_count_ > 0) {
            DCHECK(!pending_eos_set_.empty());
            return;
        }

        state_ = kPaused;
        base::ResetAndReturn(&pause_cb_).Run();

        return;
    }

    case kPaused:
    case kUninitialized:
    case kEnded:
        NOTREACHED();
        return;
    }

    base::TimeDelta start = text_cue->timestamp();

    if (state->text_ranges_.AddCue(start)) {
        base::TimeDelta end = start + text_cue->duration();

        state->text_track->addWebVTTCue(start, end,
            text_cue->id(),
            text_cue->text(),
            text_cue->settings());
    }

    if (state_ == kPlaying) {
        Read(state, text_stream);
        return;
    }

    if (pending_read_count_ == 0) {
        DCHECK_EQ(state_, kPausePending);// << "state_ " << state_;
        state_ = kPaused;
        base::ResetAndReturn(&pause_cb_).Run();
    }
}

void TextRenderer::OnAddTextTrackDone(DemuxerStream* text_stream,
    std::unique_ptr<TextTrack> text_track)
{
    DCHECK(task_runner_->BelongsToCurrentThread());
    DCHECK_NE(state_, kUninitialized);
    DCHECK(text_stream);
    DCHECK(text_track);

    std::unique_ptr<TextTrackState> state(
        new TextTrackState(std::move(text_track)));
    text_track_state_map_[text_stream] = std::move(state);
    pending_eos_set_.insert(text_stream);

    if (state_ == kPlaying)
        Read(text_track_state_map_[text_stream].get(), text_stream);
}

void TextRenderer::Read(
    TextTrackState* state,
    DemuxerStream* text_stream)
{
    DCHECK_NE(state->read_state, TextTrackState::kReadPending);

    state->read_state = TextTrackState::kReadPending;
    ++pending_read_count_;

    text_stream->Read(base::Bind(
        &TextRenderer::BufferReady, weak_factory_.GetWeakPtr(), text_stream));
}

TextRenderer::TextTrackState::TextTrackState(std::unique_ptr<TextTrack> tt)
    : read_state(kReadIdle)
    , text_track(std::move(tt))
{
}

TextRenderer::TextTrackState::~TextTrackState()
{
}

} // namespace media
